﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Reflection;

namespace Obra.Test
{
    /// <summary>
    /// Methods to help with PowerShell unit testing.
    /// </summary>
    public static class PowerShellTestHelpers
    {
        /// <summary>
        /// Register a PowerShell SnapIn via the backdoor.
        /// </summary>
        /// <param name="name">
        /// Name of the SnapIn.
        /// </param>
        /// <param name="absoluteAssemblyPath">
        /// Fully-qualified path to the </param>
        /// <param name="types"></param>
        /// <param name="formats"></param>
        /// <param name="runspaceConfiguration"></param>
        public static void RegisterSnapIn(
            string name,
            string absoluteAssemblyPath,
            List<string> types,
            List<string> formats,
            RunspaceConfiguration runspaceConfiguration)
        {
            PowerShellTestHelpers.UnregisterSnapIn(
                name, 
                runspaceConfiguration);

            PSSnapInInfo snapInInfo = PowerShellTestHelpers.GetPSSnapInInfo(
                name,
                absoluteAssemblyPath,
                types,
                formats);

            PowerShellTestHelpers.AddPSSnapInToRunspaceConfiguration(
                snapInInfo,
                runspaceConfiguration);

            PowerShellTestHelpers.LoadPSSnapIn(
                snapInInfo,
                runspaceConfiguration);
        }

        /// <summary>
        /// Unregister
        /// </summary>
        /// <param name="name">
        /// Name of the SnapIn to unregister.
        /// </param>
        /// <param name="runspaceConfiguration">
        /// RunspaceConfiguration to update.
        /// </param>
        private static void UnregisterSnapIn(
            string name,
            RunspaceConfiguration runspaceConfiguration)
        {
            try
            {
                PSSnapInException removeSnapInException;
                runspaceConfiguration.RemovePSSnapIn(name, out removeSnapInException);
            }
            catch (Exception)
            {
                // ignored
            }
        }


        /// <summary>
        /// Forcefully add a PSSnapInInfo to a runspace configuration.
        /// </summary>
        private static void AddPSSnapInToRunspaceConfiguration(
            PSSnapInInfo snapInInfo,
            RunspaceConfiguration runspaceConfiguration)
        {
            object consoleInfo = PowerShellTestHelpers.GetMshConsoleInfo(runspaceConfiguration);

            var properties = consoleInfo.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Instance);

            Collection<PSSnapInInfo> snapIns = (Collection<PSSnapInInfo>)consoleInfo.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Instance).First(
                x => x.Name == "PSSnapIns").GetValue(consoleInfo, null);

            snapIns.Add(snapInInfo);
        }

        /// <summary>
        /// Get a PSSnapInInfo from the specified assembly.
        /// </summary>
        /// <param name="snapInName">
        /// Name of the SnapIn.
        /// </param>
        /// <param name="fullyQualifiedAssemblyPath">
        /// Absolute path to the SnapIn assmebly.
        /// </param>
        /// <param name="types">
        /// List of types managed by this SnapIn.
        /// </param>
        /// <param name="formats">
        /// List of formats managed by this SnapIn.
        /// </param>
        /// <returns>
        /// PSSnapInInfo based on the assembly.
        /// </returns>
        private static PSSnapInInfo GetPSSnapInInfo(
            string snapInName,
            string absoluteAssemblyPath,
            List<string> types,
            List<string> formats)
        {
            Assembly assembly = Assembly.LoadFile(absoluteAssemblyPath, null);

            ConstructorInfo constructorInfo = typeof(PSSnapInInfo).GetConstructor(
                BindingFlags.NonPublic | BindingFlags.Instance,
                null,
                new Type[] {
                    typeof(string)/*name*/,
                    typeof(bool)/*isDefault*/,
                    typeof(string)/*applicationBase*/,
                    typeof(string)/*applicationName*/,
                    typeof(string)/*moduleName*/,
                    typeof(Version)/*psVersion*/,
                    typeof(Version)/*version*/,
                    typeof(Collection<string>)/*types*/,
                    typeof(Collection<string>)/*formats*/,
                    typeof(string)/*descriptionFallback*/,
                    typeof(string)/*vendorFallback*/,
                    typeof(string)/*customPSSnapInType*/},
                null);

            Collection<string> myTypes = new Collection<string>(types);
            Collection<string> myFormats = new Collection<string>(formats);

            object[] parameters = new object[] {
                snapInName/*name*/,
                true/*isDefault*/,
                System.IO.Path.GetDirectoryName(absoluteAssemblyPath)/*applicationBase*/,
                assembly.FullName/*assemblyName*/,
                absoluteAssemblyPath/*moduleName*/,
                new Version(2, 0)/*psVersion*/,
                assembly.GetName().Version/*verison*/,
                myTypes/*types*/,
                myFormats/*formats*/,
                String.Format("{0} SnapIn for Unit Teting", snapInName)/*descriptionFallback*/,
                "nUnit"/*vendorFallback*/,
                null/*customPSSnapInType*/};

            PSSnapInInfo info = (PSSnapInInfo)constructorInfo.Invoke(
                BindingFlags.NonPublic | BindingFlags.Instance,
                null,
                parameters,
                CultureInfo.CurrentCulture);

            return info;
        }

        /// <summary>
        /// Get the ConsoleInfo property of a RunspaceConfigurtion.
        /// </summary>
        /// <remarks>
        /// <para>
        /// ConsoleInfo holds the collection of the PSSnapIns for a 
        /// RunspaceConfiguration.
        /// </para>
        /// </remarks>
        private static object GetMshConsoleInfo(
            RunspaceConfiguration configuration)
        {
            object consoleInfo = configuration.GetType().GetProperties(
                BindingFlags.NonPublic | BindingFlags.Instance).First(
                    x => x.Name == "ConsoleInfo").GetValue(configuration, null);

            return consoleInfo;
        }  

        /// <summary>
        /// Load a PSSnapInInfo
        /// </summary>
        /// <remarks>
        /// <para>
        /// After a little bit of debugging I figured out this method is 
        /// necessary to make everything work.  I believe it reflects on 
        /// the assembly, and pulls out pertitent data.  All of the unit
        /// tests were failing because SmugMug adds an extra parameter to
        /// New-PSDrive cmdlet (-Uri), which was not recongnized and failing.
        /// </para>
        /// </remarks>
        private static void LoadPSSnapIn(
            PSSnapInInfo snapInInfo,
            RunspaceConfiguration runspaceConfiguration)
        {
            MethodInfo methodInfo = runspaceConfiguration.GetType().GetMethod(
                "LoadPSSnapIn",
                BindingFlags.NonPublic | BindingFlags.Instance,
                null,
                new Type[] { typeof(PSSnapInInfo) },
                null);

            methodInfo.Invoke(runspaceConfiguration, new object[] { snapInInfo });
        }
    }
}
